/*
 * Copyright (C) 2012-2017 DataStax Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datastax.driver.core;

import com.datastax.driver.core.exceptions.InvalidTypeException;
import com.google.common.collect.ImmutableList;

import java.util.Arrays;
import java.util.List;

/**
 * A tuple type.
 * <p/>
 * A tuple type is a essentially a list of types.
 */
public class TupleType extends DataType {

    private final List<DataType> types;
    private final ProtocolVersion protocolVersion;
    private volatile CodecRegistry codecRegistry;

    TupleType(List<DataType> types, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
        super(DataType.Name.TUPLE);
        this.types = ImmutableList.copyOf(types);
        this.protocolVersion = protocolVersion;
        this.codecRegistry = codecRegistry;
    }

    /**
     * Creates a "disconnected" tuple type (<b>you should prefer
     * {@link Metadata#newTupleType(DataType...) cluster.getMetadata().newTupleType(...)}
     * whenever possible</b>).
     * <p/>
     * This method is only exposed for situations where you don't have a {@code Cluster}
     * instance available. If you create a type with this method and use it with a
     * {@code Cluster} later, you won't be able to set tuple fields with custom codecs
     * registered against the cluster, or you might get errors if the protocol versions don't
     * match.
     *
     * @param protocolVersion the protocol version to use.
     * @param codecRegistry   the codec registry to use.
     * @param types           the types for the tuple type.
     * @return the newly created tuple type.
     */
    public static TupleType of(ProtocolVersion protocolVersion, CodecRegistry codecRegistry, DataType... types) {
        return new TupleType(Arrays.asList(types), protocolVersion, codecRegistry);
    }

    /**
     * The (immutable) list of types composing this tuple type.
     *
     * @return the (immutable) list of types composing this tuple type.
     */
    public List<DataType> getComponentTypes() {
        return types;
    }

    /**
     * Returns a new empty value for this tuple type.
     *
     * @return an empty (with all component to {@code null}) value for this
     * user type definition.
     */
    public TupleValue newValue() {
        return new TupleValue(this);
    }

    /**
     * Returns a new value for this tuple type that uses the provided values
     * for the components.
     * <p/>
     * The numbers of values passed to this method must correspond to the
     * number of components in this tuple type. The {@code i}th parameter
     * value will then be assigned to the {@code i}th component of the resulting
     * tuple value.
     *
     * @param values the values to use for the component of the resulting
     *               tuple.
     * @return a new tuple values based on the provided values.
     * @throws IllegalArgumentException if the number of {@code values}
     *                                  provided does not correspond to the number of components in this tuple
     *                                  type.
     * @throws InvalidTypeException     if any of the provided value is not of
     *                                  the correct type for the component.
     */
    public TupleValue newValue(Object... values) {
        if (values.length != types.size())
            throw new IllegalArgumentException(String.format("Invalid number of values. Expecting %d but got %d", types.size(), values.length));

        TupleValue t = newValue();
        for (int i = 0; i < values.length; i++) {
            DataType dataType = types.get(i);
            if (values[i] == null)
                t.setValue(i, null);
            else
                t.setValue(i, codecRegistry.codecFor(dataType, values[i]).serialize(values[i], protocolVersion));
        }
        return t;
    }

    @Override
    public boolean isFrozen() {
        return true;
    }

    /**
     * Return the protocol version that has been used to deserialize
     * this tuple type, or that will be used to serialize it.
     * In most cases this should be the version
     * currently in use by the cluster instance
     * that this tuple type belongs to, as reported by
     * {@link ProtocolOptions#getProtocolVersion()}.
     *
     * @return the protocol version that has been used to deserialize
     * this tuple type, or that will be used to serialize it.
     */
    ProtocolVersion getProtocolVersion() {
        return protocolVersion;
    }

    CodecRegistry getCodecRegistry() {
        return codecRegistry;
    }

    void setCodecRegistry(CodecRegistry codecRegistry) {
        this.codecRegistry = codecRegistry;
    }


    @Override
    public int hashCode() {
        return Arrays.hashCode(new Object[]{name, types});
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof TupleType))
            return false;

        TupleType d = (TupleType) o;
        return name == d.name && types.equals(d.types);
    }

    /**
     * Return {@code true} if this tuple type contains the given tuple type,
     * and {@code false} otherwise.
     * <p/>
     * A tuple type is said to contain another one
     * if the latter has fewer components than the former,
     * but all of them are of the same type.
     * E.g. the type {@code tuple<int, text>}
     * is contained by the type {@code tuple<int, text, double>}.
     * <p/>
     * A contained type can be seen as a "partial" view
     * of a containing type, where the missing components
     * are supposed to be {@code null}.
     *
     * @param other the tuple type to compare against the current one
     * @return {@code true} if this tuple type contains the given tuple type,
     * and {@code false} otherwise.
     */
    public boolean contains(TupleType other) {
        if (this.equals(other))
            return true;
        if (other.types.size() > this.types.size())
            return false;
        return types.subList(0, other.types.size()).equals(other.types);
    }

    @Override
    public String toString() {
        return "frozen<" + asFunctionParameterString() + ">";
    }

    @Override
    public String asFunctionParameterString() {
        StringBuilder sb = new StringBuilder();
        for (DataType type : types) {
            sb.append(sb.length() == 0 ? "tuple<" : ", ");
            sb.append(type.asFunctionParameterString());
        }
        return sb.append(">").toString();
    }
}
